
Effective C++ (2005)
Accustoming Yourself to C++
Item-1: View C++ as a federation of languages
C++ encapsulates multiple programming paradigms Object Oriented, Template Metaprogramming, Functional, Generic. It is more of a federation of languages.
Item-2: Prefer const enum and inline to #define
Prefer compiler over preprocessor
Don’t
Consider the following piece of code
#define ASPECT_RATIO 1.653The issue is that the symbolic name “ASPECT RATIO” is never visible to compiler as it get replaced by the preprocessor everywhere within the code making it does not being registered on symbol table. This makes it interpret the errors during compilation as it’ll refer to 1.653 rather than ASPECT RATIO, also makes hard in symbolic debugging as there is no corresponding symbol.
Do
const double ASPECT_RATIO = 1.653;This can not only register in symbol table but also allows compiler to dispatch optimisations.
2 Special cases to consider when using const in place of #define
Constant pointers
It is advised to do a const pointer pointing to a const object i.e.
const char *const name = "Swayam"; // OR const std::string name("Swayam");Class-Specific constants
To ensure the scope of constant to a class, we need to make it a member and also to make sure there are no duplicate copies, make it
staticclass GamePlayer{ private: static const int NUM_TURNS = 5; // constant declaration int score[NUM_TURNS]; }An important point here is that the above is constant declaration (not definition). C++ requires programmer to provide the definition for anything we use, but class-specific constants that are
staticand of integral type (int, char, bool) are exception.As long as we don’t use their address, we can declare them and use them without providing a definition
The definition can be provided as follows (must need to be at the namespace scope level)
const int GamePlayer::NUM_TURNS;Not providing any value as it is being already initialized at declaration. For any other (non-integral data type) initialization at declaration time can give error (Can use
constexprto achieve that)#include<iostream> class A { private: static const double f; // constexpr static const double f = 10.9; // this also works public: void print() { std::cout << this->f << std::endl; } }; const double A::f = 10.9; // if not then get linking error that undefined reference int main() { A a = A(); a.print(); return 0; }Note that cannot use
#definefor class scoped constants, as those macros are entire lifetime unless manually being#undefnor they agree to encapsulation like private or publicThe enum trick
enumvalues are compile-time constants built into the type system, no storage needed. Consider the following code#include<iostream> class A { private: static const int NUM; int scores[NUM]; }; const int A::NUM = 10Here
NUMneeds to be known at compile time to create the array of NUM size but that is defined later in the scope. We can use the enum hack for this#include<iostream> class A { private: enum {NUM = 10}; int scores[NUM]; };This is worth knowing as
- It is more like
#definethanconst, i.e. it is legal to take the address of a const but it is not legal to take the address of an#definemacro as well as same forenum - Good compilers usually don’t allocate storage for compile time constants, unless user is access the address somewhere in code
- Other reason for worth knowing is that lots of code use this, so need to be aware.
- It is more like
Another major issue using
#definedirective is using it to implement macros that look like functions but that don’t incur the overhead of a function call.// call f with the maximum of a and b #define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))This is very painful to think even, you have to remember to parenthesize all the arguments in the macro body. I prefer it doing as
template <typename T> inline void CALL_WITH_MAX(const T &a ,const T &b){ f (a > b ? a : b) }
Item-3: Use const whenever possible
const and pointers
Consider the following code
const int *p; // non-const pointer to const int data
int *const p; // const pointer to non-const int data
int const *p; // non-const pointer to const int data
const int *const p; // const pointer to const int dataRule of thumb: const applies to whatever the immediate left to it, if nothing then whatever is immediate right to it.
Function declarations
It is a good habit to keep the const constraints on the function declaration to keep the consistency with the client. Also returning a const from a function might not make sense but sometimes it can be a good habit
class Rational {...};
const Rational operator* (const Rational &lhs, const Rational &rhs);
// this can prevent the doings like
// (a*b) = c although sometimes this is what a user wants but unlikely from a user-defined typeconst Member functions
The purpose of const member functions is to operate on the const objects (they can also call by the non-const objects) but gurantees to not modify the object’s properties. The 2 variants can be overloaded to ensure the seprate functionalities
class MyVector {
private:
int* data;
size_t size;
public:
MyVector(size_t s) : size(s) {
data = new int[s];
}
// Non-const version - returns modifiable reference
int& operator[](size_t index) {
std::cout << "Non-const version called\n";
return data[index];
}
// Const version - returns read-only reference
const int& operator[](size_t index) const {
std::cout << "Const version called\n";
return data[index];
}
~MyVector() { delete[] data; }
};
int main() {
MyVector vec(5); // non-const object
const MyVector constVec(5); // const object
vec[0] = 10; // ✅ Calls non-const version, returns int&
int x = vec[0]; // ✅ Calls non-const version (but we can't modify)
int y = constVec[0]; // ✅ Calls const version, returns const int&
// constVec[0] = 20; // ❌ ILLEGAL - const version returns const int&
}There are 2 notions of defining constness
bitwise const: No member variables are modified (this is what c++ follows)
class CTextBlock { public: char& operator[](std::size_t position) const { return pText[position]; } private: char *pText; // The POINTER itself is not modified }; int main() { const CTextBlock ctb("Hello"); ctb[0] = 'J'; // ✅ Compiles! But we just modified a const object! // Now the "const" object "ctb" contains "Jello" instead of "Hello" }logical constness The object’s observable state doesn’t change (this is what violated above). There can be cases when a method cannot be bitwise const but logically it can make sense
class CTextBlock { public: ... std::size_t length() const; private: char *pText; std::size_t textLength; // last calculated length of textblock bool lengthIsValid; // whether length is currently valid }; std::size_t CTextBlock::length() const { if (!lengthIsValid) { textLength = std::strlen(pText); // error! can't assign to textLength lengthIsValid = true; // and lengthIsValid in a const } // member function return textLength; }It seems fine “logically” even for a const object but compiler won’t agree as it violates the bitwise constness. Solution is in the next subsection
mutable
mutable keyword frees the non-static data members from the constraints of bitwise constness.
class CTextBlock {
public:
...
std::size_t length() const;
private:
char *pText;
mutable std::size_t textLength; // it can vary
mutable bool lengthIsValid; // this too, even in const member functions
};
std::size_t CTextBlock::length() const
{
if (!lengthIsValid) {
textLength = std::strlen(pText); // now fine
lengthIsValid = true; // also fine
}
return textLength;
}Casting away the constness
Consider one more scenario, mutable is actually good to solve some certain mutability within const methods, but still usually we have 2 overloaded function of operator[] one returning the direct alias and other returning an const alias for const objects. Situation can comes in that both of these oeprators perform some more work like, reading data, manipulting it, some conditional checks, some logging, etc. This can lead to redundant code duplicate as both performing same thing. One solution is to one or more private methods and call them but still the calls are duplicated, so the idea what if we implement one operator[] and use it twice
It is a bad idea, but sometimes can be taken as a pinch of salt
So one might try
char& operator[](std::size_t position) {
return (*this)[position]; // ❌ INFINITE RECURSION!
// Calls itself, not the const version
}Here *this is still the same non-const object hence everytime it is going to call itself, following is the step by step understanding of right method using const_cast
// step 0: Have the const version implemented
// step 1: cast the non-const object (*this) to const
static_cast<const TextBlock&>(*this);
// step 2: now call the [] operator (should call the const version)
static_cast<const TextBlock&>(*this)[position]; // this returns `const char&`
// step 3: Remove const from returned value
const_cast<char &>(
static_cast<const TextBlock&>(*this)[position]
);
============================================================================================
// In end it should be like
const char & operator[](std::size_t position) const {
.... // preprocessing
return text[position];
}
char & operator[](std::size_t position) {
return const_cast<char &>(
static_cast<const TextBlock&>(*this)[position]
);
}Note-1: if *this is already a const object then you CANNOT do static_cast<TextBlock &>(*this); i.e. you cannot remove the constness, use const_cast for that
Note-2: having the non-const implemented and calling the const will get compiled but again, there are chances that implemented non-const can modify the properties, so always call const from a non-const
Item-4: Make sure that objects are initialized before they’re used
Its my own experience in working with different kernels and Operating Systems, please initialize after allocation, else a heisenbug will be waiting for you
Reading uninitialized values is an undefined behaviour
So there are rules that decide when object initialization is guranteed to take place and when not, but those rules are complex. So just initialize them if you are going to use them.
Difference between assignment and initialization
Consider the following code
class PhoneNumber {...};
class ABEntry {
public:
ABEntry(const std::string & name, const std::string &address, const std::list<PhoneNumber> &phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string & name, const std::string &address, const std::list<PhoneNumber> &phones) {
theName = name; // all these are not initializations, they are assignments
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}It may work with the values we expect but THIS IS NOT INITIALIZATION, IT IS ASSIGNMENT.
The rule is that, data members of an object are initialized before the body of the constructor is entered
(This isn’t true for built-in types though)
A better way is to use Member Initialization List instead of assignments, remember the initialization order must follow the declaration order of members
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
: theName(name),
theAddress(address), // these are now all initializations
thePhones(phones),
numTimesConsulted(0)
{} // the ctor body is now emptyEarlier with constructor assignment, the compiler will call the default constructor and then use copy assignment operator to override the values. With Member Initialization list, all the members are initialized using the copy-constructor of corresponding objects.
We can also use the same for default constructor
ABEntry::ABEntry()
:theName(), // call theName's default ctor;
theAddress(), // do the same for theAddress;
thePhones(), // and for thePhones;
numTimesConsulted(0) // but explicitly initialize
{} // numTimesConsulted to zeroObjects like const and references must be initialized, they can’t be assigned, hence here the use of member initialization list method is a must
Order of initialization of non-local static objects defined in different translation unit
- A static object is one that exists from the time it’s constructed until the end of the program
- Stack and heap-based objects are thus excluded. Included are global objects, objects defined at namespace scope, objects declared static inside classes, objects declared static inside functions, and objects declared static at file scope
- Static objects inside functions are known as local static objects (because they’re local to a function) and the other kinds of static objects are known as non-local static objects
- Static objects are automatically destroyed when the program exits i.e destructor is called on
mainfunction exit - A translation unit is basically, source file + all of its
#includefiles
So the issue is, lets say for initialization our logic depends on the initialization of some other “non-local static object in another translation unit” then there is no way to gurantee that it is being already initialized and good to use. This is actually undefined.
// File: logger.cpp
class Logger {
public:
void log(const std::string& msg) { /* ... */ }
};
Logger globalLogger; // Non-local static object
// File: database.cpp
#include "logger.h"
extern Logger globalLogger;
class Database {
public:
Database() {
globalLogger.log("Database initialized"); // ❌ DANGER!
// What if globalLogger isn't initialized yet?
}
};
Database globalDB; // Another non-local static objectSolution? Function-Local Static Objects wrap those non-static into a function and convert them into a local-static objects, that way refer to that function and it is being guranteed to be initialized
// File: logger.cpp
class Logger {
public:
void log(const std::string& msg) { /* ... */ }
};
// Replace global static with function returning local static
Logger& getLogger() {
static Logger instance; // Local static - initialized on first call
return instance;
}
// File: database.cpp
Logger& getLogger(); // Declaration
class Database {
public:
Database() {
getLogger().log("Database initialized"); // ✅ SAFE!
// getLogger() ensures Logger is initialized before we use it
}
};
Database& getDatabase() {
static Database instance; // Local static
return instance;
}C++11 and further guarantees that Static local initialization is thread-safe
Logger& getLogger() {
static Logger instance; // ✅ Thread-safe since C++11!
return instance;
}Constructors, Destructors and Assignment Operators
Item-5: Know what functions C++ silently writes and calls
For a C++ class (if not created) compiler can implicitly declare their own versions of copy constructor, copy assignment operator, destructor furthermore if there is no constructor then it’ll also declare a default constructor and all of these declarations will be public and inline. There are cases when this is not possible as
#include <iostream>
#include <string>
class A
{
int &ref;
public:
A(int &v) : ref(v) {}
};
int main() {
int i1 = 1;
int i2 = 2;
A a1 = A(i1);
A a2 = A(i2);
a1 = a2;
}Here the privarte class member ref is an reference which cannot be just assigned to a new object leading to a ill-formed default definition, hence compiler will give an error here. Same goes for const members. In these cases users are expected to define their own. Finally compilers reject the implicit copy assignment operators in derived classes that inherit from base classes declaring their copy assignment operator private
Item-6: Explicitly disallow the use of compiler-generated functions you do not want
In case we don’t want compiler to define the functions implictly we can just declare them in the private
class A
{
int ref;
A& operator=(const A&);
public:
A(int v) : ref(v) {}
};And in case if within some friend function or member tries to do use the copy assignment operator then it’ll give the linker-error. Although its good to convert linking errors in conpile time so here in this case we can create a base class
class Uncopyable {
protected:
Uncopyable() = default;
~Uncopyable() = default;
private:
Uncopyable(const Uncopyable&); // declared, no definition
Uncopyable& operator=(const Uncopyable&); // declared, no definition
};And inherit this in the derived class.
In new C++-11 we can also use delete keyword, which always gives compilation error (better option)
Item-7: Declare Destructors virtual in polymorphic base classes
Whoever class is designed or implemented to be inherited by others and provide virtual functions that can be overwritten by the derived classes, must implement an virtual distructor. The reason is in case the pointers to the base classes are being used to manage the derived class’s objects and in case of deleting, not having an virtual distructor will only delete the base part of that derived class leaving the instances of derived data still living and can lead to memory leak.
class TimeKeeper {
public:
~TimeKeeper() { /* cleanup for base */ }
// Notice: destructor is NON-virtual
};
class AtomicClock : public TimeKeeper {
public:
~AtomicClock() { /* cleanup for derived */ }
};
TimeKeeper* getTimeKeeper() {
return new AtomicClock(); // heap object
}
TimeKeeper* p = getTimeKeeper();
delete p; // Uh oh...Polymorphism in C++ requires a virtual destructor if you ever plan to delete derived objects via a base class pointer. Without a virtual destructor, only the base class destructor gets called when you delete through a TimeKeeper*. Fix?
class TimeKeeper {
public:
virtual ~TimeKeeper() { /* cleanup for base */ }
};Not having an virtual destructor in a base class planned to be used in polymorphism is defined as Undefined Behaviour
If a class is not meant to be used virtually (i.e. not contain any virtual functions) means it is not means to be a base calss, hence there is no need to give virtual destructor. Consider the following
class Point { // a 2D point
public:
Point(int xCoord, int yCoord);
~Point();
private:
int x, y;
};The above If an int occupies 32 bits, a Point object can typically fit into a 64-bit register. Furthermore, such a Point object can be passed as a 64-bit quantity to functions written in other languages, such as C or FORTRAN. If Point’s destructor is made virtual, however, the situation changes.
Supporting virtual dispatch forces each object to carry a vptr pointing to its class’s vtbl. That one hidden pointer bumps Point from 64 bits to roughly 96 bits on 32-bit targets and to 128 bits on 64-bit systems, so it no longer fits in a 64-bit register. It also breaks layout compatibility with a plain C struct, so you can’t pass Point across language boundaries without compensating for the vptr.
Now consider one more example
class SpecialString : public std::string {...};
SpecialString *pss = new SpecialString("bad idea");
std::string *ps;
ps = pss;
delete ps;Bad idea, All the STL containers types do not implement any virtual functions or destrutor (Its a design choice).
Now lets talk about Abstract Classes. Classes that can’t be instantiated and it has all the pure virtual functions so what if I do this
class Abstract
{
virtual ~Abstract() = 0;
};Again bad idea, although the code will compile fine. When a derived class object is destroyed, the destructor chain is called from most-derived to base class. The base class destructor ~Abstract() will be invoked, so it needs an implementation. Without it, you’ll get a linker error. So a better option is to have all the functions purely virtual except destructor
class Abstract {
public:
virtual ~Abstract() = default; // Virtual, but not pure
virtual void someFunction() = 0; // Pure virtual function
};Not all base classes are designed to be used polymorphically. Some are just inteded to be the base class like Uncopyable Item-5.
Item-8: Prevent exceptions from leaving destructors
C++ does not prohibit destructors from emitting exceptions but it certainly discourages the practice. In C++ having more than 1 active exception at a time is considered as the undefined behaviour.
How its possible to have more than 1 active exception at a time?
When an exception is thrown, the program doesn’t just “exit” immediately. It performs stack unwinding, destroying all local objects in scope while searching for a catch block. Consider following code
void doSomething() {
std::vector<Widget> v; // 10 Widgets, suppose ~Widget (destructor) throws an exception
throw std::runtime_error("Error!"); // Exception #1 thrown!
} // <-- Program counter comes here during unwindingNow when the #1 exception is thrown, it start unwinding the stack deleting all the local variables to this function, leading to call the destructor of any of the Widget instance leading to have another active exception. Undefined behaviour.
I hope the tip also explains the problem of why destructor should be encouraged to throw an exception. So what to do if they do?
- Terminate the program immediately by calling
std::abort(); - Swallow the exception i.e. just catch and keep the program working. Usually a bad practice as it hides the information of what failed.
Here Scott Meyers also proposed a method to pass the responsibility to the client/user to manually handle if such things happen. I don’t know, I am not happily agree to it. But nevertheless point to be note is that, a destructor should not throw an exception, its a very bad design.
Item-9: Never call virtual functions during construction or destruction
Suppose maintaining a transaction system and logging every type of transaction
class Transaction
{
public:
Transaction();
~Transaction();
virtual void logTransaction() const = 0; // make type-dependent log entry
};
Transaction::Transaction()
{
...
logTransaction(); // log this transaction
}
Transaction::~Transaction()
{
...;
logTransaction();
}
class BuyTransaction: public Transaction
{
public:
virtual void logTransaction() const;
};
class SellTransaction: public Transaction
{
public:
virtual void logTransaction() const;
};
BuyTransaction b; // what logTransaction this will call?In principle, in order to instantiate a derived class the parts of base class instantiate first, hence it’ll call the base class Transaction constructor which itself calls the logTransaction() method for the base class NOT the derived as the derived components aren’t being initialized yet. Same thing with the destruction, the derived class’s elements gets destroyed first hence when the base class destructor calls logTransaction it is again the type call of base not derived (BuyTransaction/SellTransaction)
It is more fundamental that that, Not only do virtual functions resolve to the base class, but the parts of the languauge using runtime information e.g. dynamic_cast or typeid also treat the object as base class type
Some compilers raise a warining about this, some don’t. In case the if the logTransaction would’ve been purely virtual (i.e. no definition in base class) then linker would’ve thrown the error (so sometimes easy to catch this)
So what to do? In such cases keep the function logTransaction non-virtual and instead recieve relevant information from derived to log
class Transaction
{
public:
explicit Transaction(std::string &logInfo);
void logTransaction(std::string &logInfo) const; // make type-dependent log entry
};
Transaction::Transaction(std::string &logInfo)
{
...
logTransaction(logInfo); // log this transaction
}
Transaction::~Transaction()
{
...;
logTransaction();
}
class BuyTransaction: public Transaction
{
private:
static std::string createLogString(parameters); // focus how this function is made static
public:
BuyTransaction(parameters); : Transaction(createLogString(parameters))
{};
};If the createLogString inside BuyTranscation wouldn’t be static then again we will be in dilemma:
BuyTransaction(parameters) : Transaction(createLogString(parameters))
// ↑ Base constructor called HERE
// The BuyTransaction object DOESN'T EXIST YET!At the point where createLogString(parameters) is called:
❌ The BuyTransaction object is not fully constructed
❌ Member variables are not initialized
❌ The this pointer is not valid for the derived class
❌ Cannot call non-static member functions
This would be undefined behavior, you’re trying to use this->createLogString() before this is valid.
static std::string createLogString(parameters);
// ↑ Static = doesn't need 'this' pointer
// Can be called like a regular function
// Doesn't access any instance dataStatic functions:
✅ Don’t need a this pointer
✅ Can be called before object construction
✅ Work like regular functions, just scoped to the class
Item-10: Have assignment operator return a reference to *this
Assignment operation returns a reference to its left hand operand and it is right associative
int x, y, z;
x = y = z = 15; // x = (y = ( z = 15 ))Here, 15 is assigned to z, then the result of that assignment (the updated z) is assigned to y, then the result of that assignment (the updated y) is assigned to x. We should also need to follow this convention for our classes
class Widget
{
public:
Widget& operator=(const Widget& rhs)
{
... // copy logic
return *this;
}
};This convention should follow for all the types of assignment operators like +=, *=, /= etc. Not following convention will compile the code but it may not be as useful with other C++ language features as all built-in as well as STL follow this convention. Do thi only if there is a strong reasoning.
Item-11: Handle assignment to self in operator=
It is very possible to perform a self-assign operation in C++
a[i] = a[j]; // i and j might be same
class Base {..};
class Derived: public Base { ... };
void doSomething(const Class& rb, Derived& pd); // they both can be pointing to same objectTake example of the following code and it’ll show it can be disastrous. We are managing the resource here
class Bitmap {...};
class Widget
{
private:
Bitmap *pb; // pointer to heap allocated Bitmap object
};
Widget&
Widget::operator=(const Widget& rhs)
{
delete pb; // stop using current butmap, free It
pb = new Bitmap(*rhs.pb);
return *this;
}Now suppose if you do the self-assignment operator here then the delete pb statement will delete both source’s as well as the RHS’s bitmap. So how to solve? well an obvious way is to an identity check as
Widget&
Widget::operator=(const Widget& rhs)
{
if (this == &rhs) // identity check
return *this;
delete pb; // stop using current butmap, free It
pb = new Bitmap(*rhs.pb);
return *this;
}There is one more issue in above code, it is also NOT exception-safe i.e. if the call to new failed (maybe because of not enough memory) then we are again left with the invalid pointers. Happily making operator= exception-safe also makes it self-assignment-safe too.
Widget&
Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig; // delete the original
return *this;
}This may not be efficient (can add the identity check) but it works as now we have a backup copy. But still an even better approach is Copy And Swap
Widget&
Widget::operator=(const Widget& rhs)
{
Widget temp(rhs);
swap(temp); // exchange this and rhs's data
return *this; // destructor of temp will clean the old pb
}Item-12: Copy all parts of an object
Tl;dr copy each and everything in the respective class
class Customer
{
public:
Customer(const Customer &rhs);
Customer& operator=(const Customer& rhs);
private:
std::string name;
};
Customer::Customer(const Customer& rhs) : name(rhs.name)
{
logCall("Customer copy constructor");
}
Customer& Customer::operator=(const Customer& rhs)
{
logCall("Customer copy assignment operator");
name = rhs.name;
return *this;
}
class PriorityCustomer: public Customer
{
public:
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
private:
int priority;
}
// This is bad
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs): priority(rhs.priority)
{
logCall("PriorityCustomer Copy Constructor");
}
// This is too
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
priority = rhs.priority;
return *this;
}For PriorityCustomer class in case of the copy-constructor call, issue is the base class’s elements aren’t getting copied so they will be initialized via the default constructor. And in case of the copy-assignment operator call the base class elements remains to original values. We explicitly need to call their copy functions as well
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs): Customer(rhs), priority(rhs.priority)
{
logCall("PriorityCustomer Copy Constructor");
}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs); // assign base class parts
priority = rhs.priority;
return *this;
}Resource Management
Item-13: Use objects to manage resources
We typically don’t want to rely on user front logic to free the acquired resources since lots of things can go wrong, consider the following example
class Investment {...};
// suppose a factory function is used to instantiate the Investment
Investment* createInvestment(); // return ptr to dynamically allocated object
void f()
{
Investment* pInv = createInvestment();
... // use pInv
delete pInv;
}Its very much possible that something bad can happen in the ... area like exception throwm, an imature return statement or anything that can lead the program logic to not encounter the delete statement. It’ll be better in that cases to free the object as it loses from the scope automatically. C++’s auto_ptr does exactly that
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
...
// it is guranteed that the destructor wil run for auto_ptr and free the holding resource as well
}This is often called RAII (Resource Acquisition is Initialization) although it is possible that destructing Investment object leads to exception but we already saw in earlier items that this is a bad thing to do.
The thing to note here is that not more than one auto_ptr can point to the same object as that way they all will try to delete the same memory. To ensure this auto_ptr does an bizzare behaviour of transfering ownership on copy
auto_ptr<Investment> pInv1(createInvestment()); // pInv1 owns the object
auto_ptr<Investment> pInv2(pInv1); // Copy constructor called
// pInv1 is now NULL
// pInv2 now owns the object
pInv1 = pInv2; // Copy assignment called
// pInv2 is now NULL
// pInv1 owns the object againThis violates the normal C++ copy semantics! After copying both the original and copy shouild be valid and independent. This also makes it incompatible to work with STL
vector<auto_ptr<Investment>> vec;
vec.push_back(auto_ptr<Investment>(createInvestment()));
auto_ptr<Investment> ptr = vec[0]; // Copies from vector
// vec[0] is now NULL!
// Vector contains a broken element!
// Sorting could destroy elements:
sort(vec.begin(), vec.end()); // Copying during sort leaves NULLs everywhere!So how to solve? We need Reference-Counting Smart Pointer (RCSP). An RCSP is a smart pointer that keeps track of how many objects point to a particular resource and automatically deletes the resource when nobody is pointing to it any longer. As such, RCSPs offer behavior that is similar to that of garbage collection. Unlike garbage collection, however, RCSPs can’t break cycles of references (e.g., two otherwise unused objects that point to one another).
void f()
{
...
std::tr1::shared_ptr<Investment> pInv1(createInvestment()); // pInv1 points to the object returned from createInvestment
std::tr1::shared_ptr<Investment> pInv2(pInv1); // both pInv1 and pInv2 now point to the object
pInv1 = pInv2; // ditto — nothing has
// changed
...
} // pInv1 and pInv2 are destroyed, and the object they point to is automatically deletedDon’t be misled, though. This Item isn’t about auto_ptr, tr1::shared_ptr, or any other kind of smart pointer. It’s about the importance of using objects to manage resources. auto_ptr and tr1::shared_ptr are just examples of objects that do that. Down the line auto_ptr and tr1::shared_tr insid their destructor uses the delete so we cannot free the dynamically allocted arrays and the reason is that vector and string can almost always replace dynamically allocated arrays. Sadly even doing that will get successfully compiled.
// following are the bad ideas to do
std::auto_ptr<std::string> aps(new std::string[10]);
std::tr1::shared_ptr<int> spi(new int[10]);Item-14: Think carefully about copying behaviour in resource-managing classes
Not all resources are heap-based that require deletion and for such resources smart pointers might not always be the best resource handlers. We might need to create our own resource-managing classes from time to time. Consider the following example of Mutex. We need to lock it and ensure it gets unlock after the use.
class Lock
{
private:
Mutex *mutexPtr;
public:
explicit Lock(Mutex *pm) : mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
}Clients can use Lock in the conventional RAII fashion
Mutex m;
{
// local scope to creating critical section
Lock m1(&m);
... // do the stuff
} // as scope ends, it'll get unlockedOne big question a programmer should ask when developing RAII classes that what happen if the object is copied? And there are different answeres to this depending on situations
Prohibit Copying Many cases it won’t make sense to allow RAII objects to be copied (like in our
Lockclass) because it rarely make sense to have copies of synchronization primitives.class Lock { private: Mutex *mutexPtr; public: explicit Lock(Mutex *pm) : mutexPtr(pm) { lock(mutexPtr); } ~Lock() { unlock(mutexPtr); } Lock& operator=(const Lock &) = delete; // copy assign operator Lock(const Lock &) = delete; // copy constructor };Reference Count the underlying resource Sometimes it’s desirable to hold on the resource unless the last object holding it gets destroyed. RAII objects should increment the count of the numbers of objects referring to the resource. In
tr1::shared_ptrwe can pass custom deleter function to change the behaviour of what to do when object goes out of scope.class Lock { private: std::tr1::shared_ptr<Mutex> mutexPtr; public: explicit Lock(Mutex *pm) : mutexPtr(pm, unlock) // unlock can be the custom function to call { lock(mutexPtr.get()); } ~Lock() { unlock(mutexPtr); } }Copy the underline resource Sometimes you want this deepcopy to ensure each copy is responsible for freeing the respective resource. Some
stringimplementation follow this i.e. during copy heap memory is allocated, characters are being copiedTransfer Ownership of the underlying resource On rare occasions, we want only one RAII object refer to the raw resource and when it gets copied, the ownership of the resource is transferred from the copied object to the copying one.
Item-15: Provide access to raw resource in resource-managing classes
Sometimes we need to deal with API that uses the raw pointers to the resource. In that case we need a balance between RAII like smart pointers and using raw. There can be 2 cases of usage, explicit conversion and implicit. For explicit its straightforward
std::tr1::shared_ptr<Investment> pInv(createInvestment());
int daysHeld(const Investment *pi); // wants a raw pointer
int days = daysHeld(pInv); // ERROR! Can't pass smart pointer directly
int days = daysHeld(pInv.get()); // Explicitly extract raw pointerGoing through implicit conversion can be sometimes look good but unknowingly increases the chances of doing mistakes. Recommend is to avoid implicit conversions and be as explicit as needed.
Item-16: Use the same form in corresponding uses of new and delete
Tl;dr the forms of new and delete must stay consistent. i.e. if used [] with new then must use [] with `delete.
std::string *ptr1 = new std::string;
std::string *ptr2 = new std::string[10];
delete ptr1;
delete [] ptr2;Be cautious using typedef
typedef std::string AddressLines[4];
std::string *pal = new AddressLines;
delete pal; // undefined
delete [] pal; // correctItem-17: Store newed objects in smart pointers in standalone statements
Consider the following code
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
processWidget(new Widget, priority()); // badIt won’t even compile as shared_ptr has an explicit constructor so cannot do implicit conversion of raw pointer to smart shared pointer. But if we do
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());This will compile good but there are chances that this can leak resource here. The evaluation of function argument is unspecified and independent. And looking at the current code compiler needs to perform
- call
priority - call
new Widget - wrap the allocated raw pointer in shared_pointer
But what if compiler decides the following order of execution:
- ****Execute****
new Widget. - Call
priority. - Call the
tr1::shared_ptrconstructor
And here suppose the call to priority raised some exception and now since the raw pointer to newly allocated memory isn’t wrapped within shared pointer so it gets leaked. Hence always a better choice is to compute them first hand.
In modern C++ (C++17 and later), the evaluation order of function arguments is still unspecified—meaning the compiler can choose any order it wants for evaluating the separate arguments (e.g., it might evaluate the second argument before the first, or vice versa). However, the evaluations are indeterminately sequenced, so there’s no interleaving of subexpressions across arguments. Once the evaluation of a particular argument begins, all its subexpressions, side effects, and nested operations must complete fully before the evaluation of another argument can start.
This prevents the resource leak scenario from the original example, because the new Widget (a subexpression within the first argument) can’t be left dangling if an exception occurs in priority() (the second argument)—either the whole first argument evaluates safely first, or priority() throws before any part of the first argument even begins.
Designs and Declarations
Item-18: Make interfaces easy to use correctly and hard to use incorrectly
Design C++ interfaces that are easy to use correctly and hard to use incorrectly. If users make mistakes, it’s often the interface’s fault, not theirs. Let’s quickly get some example
The goal is more towards not letting the inappropriate handling of interface compile
class Date
{
public:
Date(int month, int day, int year);
...
};
Date d(30, 3, 1995); // Oops! Should be "3, 30", not "30, 3"
Date d(2, 20, 1995); // Oops! Should be "3, 30", not "2, 20"These and many client errors can be prevented by the introduction of new types. The type system is our primary ally
struct Day
{
explicit Day(int d) : val(d);
int val;
};
struct Month
{
explicit Month(int d) : val(d);
int val;
};
struct Year
{
explicit Year(int d) : val(d);
int val;
};
class Date
{
public:
Date(const Month& month, const Day& day, const Year& year);
...
};
Date d(30, 3, 1995); // error! wrong types
Date d(Day(30), Month(3), Year(1995)); // error! wrong types
Date d(Month(3), Day(30), Year(1995)); // okay, types are correct
// what if? Month class is defined in another TU and is not initialized yet?It can be more reasonable to add some constraints in our types like Month can be only between 1-12. One way could be using enum but remember enum are not as type-safe as we might like (can be used as ints). Safer solution is to predefine the set of all valid Months
class Month
{
public:
static Month Jan() {return Month(1);}
static Month Feb() {return Month(2);}
...
static Month Dec() {return Month(12);}
...
private:
int val;
explicit Month(int m) : val(m)
{}
};
Date d(Month::Mar(), Day(30), Year(1995));Better use the local-static Month objects to gurantee the initialization.
Tip-2, can be whenever in confusion, follow the behavior of built-in types like int — users already understand them.
Item-19: Treat class design as type design
Designing good classes is challenging because designing good types is challenging. Good types have a natural syntax, intuitive semantics, and one or more efficient implementations. In C++, a poorly planned class definition can make it impossible to achieve any of these goals. Even the performance characteristics of a class’s member functions may be affected by how they are declared.
How, then, do you design effective classes? First, you must understand the issues you face. Virtually every class requires that you confront the following questions, the answers to which often lead to constraints on your design:
How should objects of your new type be created and destroyed?
How should object initialization differ from object assignment?
What it means for objects of your new type to be passed by value?
What are the restrictions on legal values for your new type?
Does your type fit into an inheritance graph?
What type of conversions are allowed for your new type?
What operators and functions make sense for the new type?
What standard functions should be disallowed?
Who should have access to the members of your new type?
What is the “undeclared interface” of your new type?
What kind of gurantees does it offer w.r.t. performance, exception safety, resource usage.
How general is your new type?
Is a new type is really what you need?
Item-20: Prefer pass-by-reference-to-const to pass-by-value
Defult, C++ passes objects to and from functions by value. These copies are produced by object’s copy constructors which can make pass-by-value an expensive operation.
class Person
{
public:
Person();
virtual ~Person();
...
private:
std::string name;
std::string address;
};
class Student: public Person
{
public:
Student();
~Student();
...
private:
std::string schoolName;
std::string schoolAddress;
};Here imagining passing a Student object to a function will call its copy constructor, which calls the copy constructor of std::string (2x) then calls the copy constructor of the Person which further calls the copy constructor of std::string (2x) now we can see how expensive this is.
Knowing that the upstream function might not going to modify the object itself we can just pass the const reference
bool validateStudent(const Student& s);
Student plato;
bool platoIsOK = validateStudent(plato);We need to be aware that in pass-by-value, if we are passing an object of derived class but the function parameter stores it in the base class, then the derived component will be stripped as this is a copy made by the base class’s copy constructor. In such cases its almost always better to pass reference.
Under the hood, C++ compiler implements refernces like pointers, so for builtin types like int, char, float its good to do the pass-by-value as the copy would just require a register but with pointer there is some extra overhead of dereferencing the pointers.
Even when small objects have inexpensive copy constructors, there can be be performance issues. Some compilers treat built-in and user-defined types differently, even if they have the same underlying representation. Some compilers refuse to put objects consisting of just a double into a register even though they happily place naked double there on regular basis.
Item-21: Don’t try to return a reference when you must return an object
The intent is clear from the title, consider the following example
class Rational
{
public:
Rational(int num = 0, int denom = 1) : n(num), d(denom)
{}
private:
int n, d;
friend
const Rational operator*(const Rational& lhs, const Rational& rhs);
};
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}Assuming that you get flow in emotions and dont want to incur the cost of returning by value (makes copy). Returning reference is very messy here 1. Suppose returned the reference of the object, but that is created within the function and its local, there is no use outside of the scope leading to UB 2. Well in that case we create it on heap and then return, OK but who’s gonna delete it? Who’s the owner of the output pointers?
Overall returning a reference is not a good thing to do here, hence the appropriate thing to do here
inline const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}Modern compilers perform Return Value Optimization (RVO), so above example is both:
- safe (no shared state)
- efficient (no unnecessary copy)
Item-22: Declare data members private
- Declare data members
private. It gives clients syntactically uniform access to data, affords fine-grained access control, allows invariants to be enforced, and offers class authors implementation flexibility. protectedis no more encapsulated thanpublic
Item-23: Prefer non-member non-friend functions to member functions
Prefer non-member non-friend functions to member functions. Doing so increases encapsulation, packaging flexibility, and functional extensibility.
Item-24: Declare non-member functions when type conversions should apply to all parameters
Assume the same previous Rational example
class Rational
{
public:
Rational(int num = 0, int denom = 1) : n(num), d(denom)
{}
const Rational operator*(const Rational& rhs)
{
return Rational(this->n * rhs.n, this->d * rhs.d);
}
private:
int n, d;
};
Rational r = Rational(1,2);
const Rational out = r * 2; // this works (since constructor is non-explicit)
const Rational out = 2 * r; // ERROR!!In C++ implicit type conversions can only happen if arguments that appear explicitly in the function’s parameter list.
class Rational
{
public:
Rational(int num = 0, int denom = 1) : n(num), d(denom)
{}
int numerator(); // getter for n
int denominator(); // getter for d
private:
int n, d;
};
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
const Rational out = 2 * r; // WORKS NOWSo should we be making this external function the friend function of Rational class? NO! In C++, a function has three possibilities:
- Member function (has access to this, part of the class API)
- Non-member function (outside the class, uses only public APIs)
- Friend function (non-member, but allowed to access private parts)
Many C++ programmers mistakely think that, “If the function is related to the class but shouldn’t be a member, then it must be a friend.”
Why avoid friend?
- A friend function can access private data → breaks encapsulation.
- More friends = more maintenance risk (changes to private members ripple out).
- Friend should be used only when necessary.
Item-25: Consider support for a non-throwing swap
swap introduced in STL and now became a mainstay of exception-safe programming. swap is so helpful makes it also important to implement properly.
For smaller types 3 copy implementation just works fine
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}This works fine as long as your type supports copying and the cost of copying is not much. For some types, copying is expensive (think: large data structures, objects holding large buffers, etc.).
For such types the design can be, a type consisting the pointer to another type that contains real data. A common manifestation of this design approach is the “pimpl idiom” (pointer to implementation)
class WidgetImpl
{
public:
....
private:
int a, b, c; // possibly lots of data
std::vector<double> v; // expensive to copy
};
class Widget
{
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs)
{
...
*pImpl = *(rhs.pImpl); // just copy the WidgetImpl pointer
}
private:
WidgetImpl* pImpl; // ptr pointing to data
};But the std::swap has no idea to swap the pointers, instead it’ll start making copies and also making copies of the Impl. BUT there is a way:
C++ allows total specialization of standard templates for types you own
You can write a custom version of
std::swapthat applies only to your type (Widget), as long as you don’t modify anything else inside namespacestd.
namespace std
{
template<>
void swap<Widget>(Widget& a, Widget& b)
{
swap(a.pImpl, b.pImpl); // just swap the pointers (won't work, they are private)
}
}Now the design choice, How to make it compile? We can define the swap inside Widget class so that it can access
class Widget
{
public:
...
void swap(Widget& other) noexcept // must not throw
{
using std::swap // enables ADL lookup for non-member swaps
swap(pImpl, other.pImpl); // just swap the pointers
}
...
private:
WidgetImpl* pImpl;
};
// Now we can specialize the std::swap for Widget
namespace std
{
template<>
void swap<Widget>(Widget* a, Widget& b) noexcept
{
a.swap(b); // call Widget's member swap
}
}Why doing using std::swap inside the class member swap implementation?
Suppose you’re writing a function template where you need to swap the values of two objects:
template<typename T>
void doSomething(T& obj1, T& obj2)
{
...
swap(obj1, obj2);
...
}Which swap should this call? The general one in std, which you know exists; a specialization of the general one in std, which may or may not exist; or a T-specific one, which may or may not exist and which may or may not be in a namespace (but should certainly not be in std)? What you desire is to call a T-specific version if there is one, but to fall back on the general version in std if there’s not. Here’s how you fulfill your desire:
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap; // make std::swap available in this function
...
swap(obj1, obj2); // call the best swap for objects of type T
...
}C++’s name lookup rules ensure that this will find any T-specific swap at global scope or in the same namespace as the type T. (For example, if T is Widget in the namespace WidgetStuff, compilers will use argument-dependent lookup to find swap in WidgetStuff.) If no T-specific swap exists, compilers will use swap in std, thanks to the using declaration that makes std::swap visible in this function.
These lookups rules are called ADL (Argument Dependent LookUp)
This specialization means: When someone writes std::swap(a, b) anda/b are Widgets, call a.swap(b). Not only does this compile, it’s also consistent with the STL containers, all of which provide both public swap member functions and specializations of std::swap that call these member functions.
Now suppose Widget and WidgetImpl are class tempalates, we may could’ve done something like
template<typename T>
class WidgetImpl {...};
template<typename T>
class Widget {...};Adding the swap member function to Widget is still easy as before, but the issues occur when specializing the std::swap, you maybe thinking doing something like
namespace std
{
template<typename T>
void swap<Widget<T>>(Widget<T>& a, // ERROR, this is illegal code
Widget<T>&b)
{
a.swap(b);
}
}Here, swap<Widget<T>> is not a full specialization, because T is still a template parameter. It is a partial specialization, which is illegal for function templates.
So when we want to “partially specialize” a function template, the usual approach is to simply add an overload
namespace std
{
template<typename T>
void swap<Widget<T>& a, Widget<T>& b) // notice it is swap() not swap<T>()
{
a.swap(b);
}
}Well still NO! in general overloading function tempaltes is fine, but std is a special namespace with strict rules. It is okay to specialize templates in std but its NOT OKAY to overload/add new templates. It may compile, but the behavior is Undefined.
So WHAT TO DO? We still need a way to let other people call swap and get out efficient template-specific version. The answer is simple. We still declare a non-member swap that calls the member swap we just don’t declare the non-member to be a specialization or overloading of std::swap Example all our Widget related functionality is in the namespace WidgetStuff
namespace WidgetStuff
{
template<typename T>
class Widget {...}; // includes the member swap function
....
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}Now just one thing need to keep in mind is that, We want the fast, type-specific swap to be called for our type (Widget or Widget<T>), not the slow default std::swap.
std::swap(obj1, obj2); // the wrong way to call swap
// The right ✅ way
using std::swap
std::swap(obj1, obj2);Implementations
Item-26: Postpone variable definitions as long as possible
Simple terms define variable only when it is REALLY needed and see if you can optimize that need. This can give some performance benefits.
std::string encryptPassword(const std::string& password)
{
using namespace std;
string encrypted;
if(password.length() < MinimumPasswordLength)
throw logic_error("Too short");
... // do encryption
return encrypted;
}Note that the object encrypted isn’t completely unused here, but unused if the exception was thrown. So we have to pay for construction, destruction on encrypted even when not using it.
std::string encryptPassword(const std::string& password)
{
using namespace std;
if(password.length() < MinimumPasswordLength)
throw logic_error("Too short");
string encrypted;
... // do encryption
return encrypted;
}We can do better, suppose there is a helper encrypt method that does all the encryption as:
void encrypt(std::string& s); // suppose it does in-place encryptionso the best way can be
std::string encryptPassword(const std::string& password)
{
using namespace std;
if(password.length() < MinimumPasswordLength)
throw logic_error("Too short");
string encrypted(password); // initalize
encrypt(encrypted);
return encrypted;
}But this is a choice, doing this without thinking can give really bad performance, eg:
// CASE-1
Widget w;
for(int i = 0; i < 10; i++)
{
w = // something depending i
}
// CASE-2
for(int i = 0; i < 10; i++)
{
Widget w(// something with i)
}Here you can see if w can be shared across i then initializing it everytime within loop might be too expensive (10 construction + 10 destruction)
Item-27: Minimizing Casting
Casting is unsafe for your strongly typed systems and C casting are very unsafe.
(T) expression // cast expression to be of type T
// or
T(expression) // functional style castingC++ offers 4 new cast forms - const_cast<T>(expression) - dynamic_cast<T>(expression) - reinterpret_cast<T>(expression) - static_cast<T>(expression)
Each serves a distinct purpose, the old C-style casts are still valid but not preferrable. Maybe an okayish reason to use old-style cast is to call an explicit constructor to pass an object to a function. For example:
class Widget
{
public:
explicit Widget(int size);
...
};
void someWork(const Widget& w);
doWork(Widget(15)); // functional style cast
doWork(static_cast<Widget>(15)); // C++ style castMany programmers believe that casts do nothing but tell compilers to treat one type as another, but this is mistaken. Type conversions of any kind (either explicit via casts or implicit by compilers) often lead to code that is executed at runtime. For example, in this code fragment,
int x, y;
double d = static_cast<double<(x) / y;The cast of int x to double certainly generates code, because on most architectures the underlying representation for an int is different from that for a double
For better understanding consider the following code:
class Base {
public:
int baseData;
};
class Other {
public:
int otherData;
};
class Derived : public Other, public Base {
public:
int derivedData;
};
/*
Inside memory it can be
+---------------------+ ← address X (pd)
| Other part |
+---------------------+
| Base part | ← address X + offset (pb)
+---------------------+
| Derived part |
+---------------------+
*/If Derived inherits from the Base the Base part usually is stored inside the the Derived object. Sometimes especially with multiple-inheritance, the Base subobject is not at the beginning of the Derived. So converting Derived to Base may require adding an offset.
Derived d;
Base* pb = &d;pb can’t point to the start of Derived (address X), it must point to the Base subobject inside Derived which is at X + sizeof(Other), hence pb != pd. The compiler automatically applies this offset.
Interesting thing about casting is that, its so easy to write wrong code that looks right. Suppose you have a derived class and you want to call the function of the base’s class so you might want to do as follows:
class Window
{
public:
virtual void onResize();
};
class SpecialWindow: public Window
{
public:
virtual void onResize()
{
static_cast<Window>(*this).onResize();
}
};So what will happen here is that it’ll compile but the code won’t call Window’s onResize on the current this pointer. It’ll call on the copy of the base class part of the current object. If Window::Resize modifies the object on which it is invoked, then those changes won’t be visible on our current object. The correct way to do is as:
class SpecialWindow: public Window
{
public:
virtual void onResize()
{
Window::onResize();
}
};const_cast<T>(expression)
Typically used to cast away the constness of objects. It is the only C++-style cast that can do this.
void modify(int* p) {
*p = 100;
}
int main() {
const int value = 42;
modify(&value); // can't pass const
// Remove constness using const_cast
int* p = const_cast<int*>(&value);
modify(p); // works
cout << "value = " << *p << endl;
return 0;
}Modifying an object that was originally declared const results in UB.
dynamic_cast<T>(expression)
dynamic_cast is use to safely convert a pointer or reference of a base class to a derived class, at runtime. It performs runtime type checking, using information from polymorphism (virtual functions).
Think of it as a way to ask:
“Is this Base* actually pointing to a Derived object?”
If it is, you get a valid pointer/reference. If not, you get nullptr (for pointers) or an exception (for references).
#include <iostream>
using namespace std;
class Base {
public:
virtual void speak() { cout << "Base speaking\n"; }
virtual ~Base() {} // Always make base destructor virtual for polymorphism
};
class Derived : public Base {
public:
void speak() override { cout << "Derived speaking\n"; }
void derivedFunction() { cout << "Derived-specific behavior\n"; }
};
int main() {
Base* basePtr = new Derived(); // Actually pointing to a Derived object
// Try to cast Base* -> Derived*
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
if (derivedPtr) { // dynamic_cast succeeded
derivedPtr->speak();
derivedPtr->derivedFunction(); // We can use Derived-only features
} else {
cout << "Cast failed\n";
}
delete basePtr;
return 0;
}In practice dynamic_casts are slow, whenever a class contains the virtual functions, the compiler attachs a vtable pointer at every object. That table contains:
- Addresses of virtual functions, and
- A pointer to the
type_infoobject representing the class.
So when you do a dynamic_cast, the runtime can:
- Look at the object’s
vtable. - Get the
type_infoof the actual type. - Walk up or across the inheritance tree to see if conversion to the target type (Derived) is valid.
In some older compiler’s implementation of dynamic_cast it performs string comparison between classes while travelling the inheritance tree, which is even more expensive operation. Overall avoid it as much as possible in the code-design.
static_cast<T>(expression)
static_cast<T>(expression) performs a compile time checked conversion. It does not use RTTI (Run Time Type Info), does not do runtime checks, and does not cost anything extra at runtime.
It tells the compiler:
“Convert this expression to type T, as long as the compiler can verify that the conversion makes sense.”
So static_cast is safe only when the conversion is logically valid. If the programmer gets it wrong, C++ will not stop you, it’ll be undefined behavior though. Some valid usecases are as:
// numeric conversions
double x = 3.14;
int y = static_cast<int>(x);
// Upcasting (Derived => Base)
Derived d;
Base *base = static_cast<Base *>(&d);Remember, downcasting (Base => Derived) is allowed but not safe, as static_cast can’t do the runtime checking so bette use dynamic_cast or avoid such scenarios at all.
// void conversions
// void* is C’s typeless pointer; static_cast can convert to/from it safely (this is well defined)
int n = 10;
void* vp = static_cast<void*>(&n);
int* ip = static_cast<int*>(vp);struct Meter {
explicit Meter(double v) : value(v) {}
double value;
};
Meter m = static_cast<Meter>(3.5); // required// Enum conversions
// From enum → int, or int → enum (but int → enum is risky).
enum Color { RED, GREEN, BLUE };
int x = static_cast<int>(RED); // OK
Color c = static_cast<Color>(2); // allowed but may be invalid logicallyWhat static_cast Cannot Do:
- It cannot remove constness (must use
const_cast). - It cannot cast between unrelated pointer types (use
reinterpret_castinstead). - It cannot perform runtime type checking (use
dynamic_cast). - It cannot do “magic type gymnastics” like in C-style casts.
reinterpret_cast<T>(expression)
reinterpret_cast performs a pure bit reinterpretation.
- It does NOT change the actual value
- It does NOT check the safety
- It does NOT perform conversion
- It does NOT guarantee round-trip correctness.
It simply tells the compiler:
“Treat these bits as if they represent another unrelated type T.”
Because of this, the standard says its behavior is mostly implementation-defined, and many uses lead to undefined behavior. So this cast is the most dangerous one.
Here is an example of how to view and use this
struct alignas(16) float4 {
float x, y, z, w;
};
float* data = static_cast<float*>(std::aligned_alloc(16, 8 * sizeof(float)));
// OR
// float* data = new (std::align_val_t(16)) float[8];
// Safe reinterpret
float4* vec = reinterpret_cast<float4*>(data);Item-28: Avoid returning “handles” to object internals.
Tl;dr don’t allow any object member function to return reference to the hidden internal. For example:
class Point
{
public:
Point(int x, int y);
void setX(int val);
void setY(int val);
private:
int x, y;
};
struct RectData
{
Point ulhc; // upper left hand corner
Point lrhc; // lower right hanc corner
};
class Rectangle
{
public:
Point& upperLeft() const { return pData->ulhc; }
Point& lowerRight() const { return pData->lrhc; }
private:
std::shared_ptr<RectData> pData;
}Here the public getter function returns a refernce to the private Point object, which can allow user to modify it, this is self contradictory. Instead follow:
class Rectangle
{
public:
const Point& upperLeft() const { return pData->ulhc; }
const Point& lowerRight() const { return pData->lrhc; }
private:
std::shared_ptr<RectData> pData;
}